/**
 * 
 */
package org.codesmell.util.rendermap;

import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;

import org.codesmell.util.rendermap.ValueRenderer.Raw;
import org.codesmell.util.rendermap.local.Assert;
import org.codesmell.util.rendermap.local.ExceptionHelper;

import com.thoughtworks.proxy.toys.decorate.Decorating;

/**
 * @author Uwe Schäfer, (schaefer@thomas-daily.de)
 */
public class RenderMapFactory implements Serializable
{
    private final TypeConverter DEFAULT_CONVERTER;
    private final ValueRenderer DEFAULT_RENDERER;
    private final MapRenderer MAP_RENDERER;

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Converter {
        Class<? extends TypeConverter>[] value();
    }
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Render { // TODO name is weird
        Class<? extends ValueRenderer> value() default Raw.class;
    }
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Name {
        String value();
    }

    public RenderMapFactory(final TypeConverter defaultConverter, final ValueRenderer defaultValueRenderer,
            final MapRenderer mapRenderer)
    {
        Assert.parametersNotNull("defaultConverter, defaultValueRenderer, mapRenderer", defaultConverter,
                defaultValueRenderer, mapRenderer);

        DEFAULT_CONVERTER = defaultConverter;
        DEFAULT_RENDERER = defaultValueRenderer;
        MAP_RENDERER = mapRenderer;
    }

    /**
     * @author Uwe Schäfer, (schaefer@thomas-daily.de)
     */
    private final class Decorator extends com.thoughtworks.proxy.toys.decorate.Decorator implements Serializable
    {
        private static final long serialVersionUID = 1L;

        private LinkedHashMap<String, String> m = new LinkedHashMap<String, String>();

        @Override
        public Object decorateResult(final Object arg0, final Method arg1, final Object[] arg2, final Object arg3)
        {
            if (arg1.getName().equals("toString"))
            {
                return MAP_RENDERER.render(m);
            }

            if (arg1.getName().equals(delegateGetterName()))
            {
                return m;
            }

            try
            {
                final Method method = Object.class.getMethod(arg1.getName(), arg1.getParameterTypes());
                return method.invoke(m, arg2);
            }
            catch (final SecurityException e)
            {// fine
            }
            catch (final NoSuchMethodException e)
            {// fine
            }
            catch (final IllegalArgumentException e)
            {
                throw ExceptionHelper.wrap(e);
            }
            catch (final IllegalAccessException e)
            {
                throw ExceptionHelper.wrap(e);
            }
            catch (final InvocationTargetException e)
            {
                throw ExceptionHelper.wrap(e);
            }

            final String name = getNameFromMethod(arg1);

            Object value = arg2[0];
            if (value == null)
            {
                m.remove(name);
            }
            else
            {
                final List<Class<? extends TypeConverter>> converters = getConverters(arg1);

                for (final Iterator<Class<? extends TypeConverter>> i = converters.iterator(); i.hasNext();)
                {
                    final Class<? extends TypeConverter> c = i.next();
                    final TypeConverter conv = newInstance(c);
                    value = conv.convert(value);
                }

                ValueRenderer r = DEFAULT_RENDERER;

                final Render annotation = arg1.getAnnotation(Render.class);
                if (annotation != null)
                {
                    r = newInstance(annotation.value());
                }

                m.put(name, r.render(value));
            }
            return arg0;
        }

        private String delegateGetterName()
        {
            final Method[] declaredMethods = StringMapWrapper.class.getDeclaredMethods();
            Assert.isTrue(declaredMethods.length == 1, StringMapWrapper.class.getName()
                    + " conatins more than one Method."); // there shall be only
            // one :)
            return declaredMethods[0].getName();
        }

        @SuppressWarnings("unchecked")
        private List<Class<? extends TypeConverter>> getConverters(final Method arg1)
        {
            final Annotation a = arg1.getAnnotation(Converter.class);
            if (a == null)
            {
                return Collections.EMPTY_LIST;
            }
            final Converter c = (Converter) a;
            final Class<? extends TypeConverter>[] classes = c.value();
            return Arrays.asList(classes);
        }

        private <T> T newInstance(final Class<? extends T> c)
        {
            try
            {
                return c.newInstance();
            }
            catch (final InstantiationException e)
            {
                throw new RuntimeException(e);
            }
            catch (final IllegalAccessException e)
            {
                throw new RuntimeException(e);
            }
        }

        protected String getNameFromMethod(final Method arg1)
        {
            final Name na = arg1.getAnnotation(Name.class);
            if (na != null)
            {
                return na.value();
            }
            return arg1.getName();
        }
    }

    private static final long serialVersionUID = 1L;

    @SuppressWarnings("unchecked")
    public <V> V create(final Class<V> v)
    {
        return Decorating.proxy(v).with(null).visiting(new Decorator()).build();
    }

}
